/*
 *  serial.c - implementing serial port communication functions
 *  Copyright (c) 2012 Neolink Co.Ltd
 */

#include <limits.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include "serial.h"

static speed_t baudrate2speed(int baudrate)
{
	switch (baudrate) {
	case 50: return B50;
	case 75: return B75;
	case 110: return B110;
	case 134: return B134;
	case 150: return B150;
	case 200: return B200;
	case 300: return B300;
	case 600: return B600;
	case 1200: return B1200;
	case 1800: return B1800;
	case 2400: return B2400;
	case 4800: return B4800;
	case 9600: return B9600;
	case 19200: return B19200;
	case 38400: return B38400;
	case 57600: return B57600;
	case 115200: return B115200;
	case 230400: return B230400;
	default: return B0;
	}
}

/**
 * open a serial port
 * @retval SERIAL_OK opening successed
 * @retval SERIAL_ERR opening failed
 */
int serial_open (
	serial_t *pfd,                       /* [out] return handle of serial */
	const char *device,                  /* [in] device name */
	unsigned int baudrate,               /* [in] baudrate */
	unsigned int bits,                   /* [in] start bits */
	unsigned int stop,                   /* [in] stop bits */
	char parity                          /* [in] 'E'- even, 'O' - odd, '\0' - null */
	)
{
	int fd = -1;
	struct termios tis;
	int ret;

	/* Open serial device */
	fd = open(device, O_RDWR | O_NONBLOCK | O_NOCTTY);
	if ( fd >= 0 ) {

		// get old terminos
		ret = tcgetattr(fd, &tis);
		if (ret < 0) {
			close(fd);
			return SERIAL_ERR;
		}

		// baudrate
		cfsetispeed(&tis, baudrate2speed(baudrate));
		cfsetospeed(&tis, baudrate2speed(baudrate));

		// databits
		tis.c_cflag &= ~CSIZE;
		switch (bits) {
		case 5:  tis.c_cflag |= CS5;  break;
		case 6:  tis.c_cflag |= CS6;  break;
		case 7:  tis.c_cflag |= CS7;  break;
		case 8:  tis.c_cflag |= CS8;  break;
		}

		// parity
		switch (parity)
		{
		case 'n':
		case 'N':
			tis.c_cflag &= ~PARENB;
			tis.c_iflag &= ~INPCK;
			break;
		case 'o':
		case 'O':
			tis.c_cflag |= (PARODD | PARENB);
			tis.c_iflag |= INPCK;
			break;
		case 'e':
		case 'E':
			tis.c_cflag |= PARENB;
			tis.c_cflag &= ~PARODD;
			tis.c_iflag |= INPCK;
			break;
		}

		// stopbits
		switch (stop) {
		case 1:  tis.c_cflag &= ~CSTOPB;  break;
		case 2:  tis.c_cflag |= CSTOPB;   break;
		}

		// flow control
		tis.c_cflag &= ~CRTSCTS;

		//tis.c_oflag = 0;
		tis.c_lflag = ~(ICANON | ECHO | ECHOE | ISIG);
		tis.c_cc[VTIME] = 0;
		tis.c_cc[VMIN] = 0;

		//flush the buffer
		tcflush(fd, TCIFLUSH);

		ret = tcsetattr(fd, TCSANOW, &tis);
		if (ret < 0) {
			if (serial_debug) {
				serial_printf("serial_open %s failed, tcsetattr error!\n",
					device
				);
			}
			close(fd);
			return SERIAL_ERR;
		}

		*pfd = fd;
	}

	if (serial_debug) {
		serial_printf("serial_open %s, baudrate %d,  0x%x -> fd[%d]\n",
			device, baudrate, tis.c_cflag, fd
			);
	}

	return (fd > 0) ? SERIAL_OK : SERIAL_ERR;
}

/**
 * flush serial buffer
 */
int serial_flush (
	serial_t fd,                         /* [in] the handle for the serial port */
	int timeout                          /* [in] timeout, millseconds waiting to read */
	)
{
	unsigned char buffer[64];
#ifdef __APPLE__
	struct 
#endif
	fd_set fdr;
	struct timeval tv = {0,0};
	int ret;

	if (serial_debug) {
		serial_printf("serial_flush fd[%d]\n", fd);
	}

	ret = SERIAL_OK;

	/* Returns 0 upon success, -1 upon failure. */
	if (tcflush(fd, TCIOFLUSH) < 0) {
		return SERIAL_ERR;
	}

	for (;;) {
		FD_ZERO(&fdr);
		FD_SET(fd, &fdr);
		tv.tv_sec = SERIAL_INTERVAL_MSEC_SPLIT_SEC(timeout);
		tv.tv_usec = SERIAL_INTERVAL_MSEC_SPLIT_USEC(timeout);
		ret = select(fd+1, &fdr, NULL, NULL, &tv);
		if ( ret < 0) {	//	failed
			ret = SERIAL_ERR;
			break;
		} else if ( ret == 0 ) {	//	timed-out
			ret = SERIAL_OK;
			break;
		} else {
			if ( FD_ISSET(fd, &fdr) ) {
				ret = read(fd, (char*)buffer, sizeof(buffer));
				if (ret < 0) {
					ret = SERIAL_ERR;
					break;
				}
				else if (ret == 0) {
					ret = SERIAL_OK;
					break;
				}
			}
		}
	}

	return ret;
}

/**
 * close the serial port
 */
void serial_close(serial_t fd)
{
	if (serial_debug) {
		serial_printf("serial_close fd[%d]\n", fd);
	}

	close(fd);
}

/**
 * write to serial port
 * @return SERIAL_OK or SERIAL_ERR
 */
int serial_write (
	serial_t fd,                         /* [in] the handle for the serial port */
	unsigned char *buffer,               /* [in] buffer the data to be written */
	unsigned int len                     /* [in] len the length of the data */
	)
{
	int w;
	unsigned char *p;
	int ret = SERIAL_OK;

	if (serial_debug) {
		log_t *lmsg = logpc(LOG_NONE);
		logp(lmsg, "serial_write fd[%d], size %d:\n", fd, len);
		logpb(lmsg, buffer, len, "%x", ",");
		logp(lmsg, "\n");
		logpf(lmsg);
	}

	p = buffer;
	while (len > 0) {
		w = write(fd, p, len);
		if ( w < 1 ) {
			ret = SERIAL_ERR;
			break;
		}
		
		len -= w;
		p   += w;
	}

	return ret;
}

/**
 * read data with specified length from the serial port
 * @retval SERIAL_OK if expected number of bytes read
 * @retval SERIAL_TIMEOUT if timed-out during the read
 * @retval SERIAL_ERR if read failed
 */
int serial_read(
	serial_t fd,                         /* [in] the handle for the serial port */
	unsigned char *buffer,               /* [in] the data to be read */
	unsigned int *len,                   /* [in,out] input the length of the data, return received data length */
	int timeout                          /* [in] millseconds waiting to read */
)
{
	int ret = SERIAL_OK;
	int r = -1;
#ifdef __APPLE__
	struct 
#endif
	fd_set fdr;
	struct timeval tv = {0,0};
	unsigned int cnt = 0;

	while (cnt < *len) {

		FD_ZERO(&fdr);
		FD_SET(fd, &fdr);

		if (timeout == -1) {
			r = select(fd + 1, &fdr, NULL, NULL, NULL);
		} else {
			tv.tv_sec = SERIAL_INTERVAL_MSEC_SPLIT_SEC(timeout);
			tv.tv_usec = SERIAL_INTERVAL_MSEC_SPLIT_USEC(timeout);
			r = select(fd + 1, &fdr, NULL, NULL, &tv);
		}

		if ( r < 0) {	//	failed
			ret = SERIAL_ERR;
			break;
		} else if ( r == 0 ) {	//	timed-out
			ret = SERIAL_TIMEOUT;
			break;
		} else {
			if ( FD_ISSET(fd, &fdr) ) {
				r = read(fd , (char*)(buffer + cnt), *len - cnt);
				if ( r >= 0 ) {
					cnt += r;
				}
			}
		}
	}

	*len = cnt;

	if ( (r >= 0) && (cnt > 0)) {
		if (serial_debug) {
			log_t *lmsg = logpc(LOG_NONE);
			logp(lmsg, "serial_read fd[%d], size %d:\n", fd, cnt);
			logpb(lmsg, buffer, cnt, "%x", ",");
			logp(lmsg, "\n");
			logpf(lmsg);
		}
	}
	else if (r == 0) {
		if (serial_debug) {
			serial_printf("serial_read fd[%d], timer %d is timeout!\n", 
				fd, timeout
				);
		}
	}
	else if (r < 0) {
		if (serial_debug) {
			serial_printf("serial_read fd[%d] fail, errcode = %d!\n", 
				fd, r
				);
		}
	}

	return ret;
}
